using System.Collections.Concurrent;
using System.ComponentModel;
using System.Net;
using System.Net.Http.Headers;
using ShellProgressBar;

namespace NalogGarLoader.Model;

public class FileDownloader
{
    private static readonly NLog.Logger log = NLog.LogManager.GetCurrentClassLogger();

    private const long KilobytesInMegabyte = 1_048_576;

    static readonly ProgressBarOptions ProcessBarOption = new()
    {
        ForegroundColor = ConsoleColor.Green,
        ForegroundColorDone = ConsoleColor.DarkGreen,
        BackgroundColor = ConsoleColor.DarkGray,
        BackgroundCharacter = '\u2593',
        EnableTaskBarProgress = true,
        ProgressBarOnBottom = false,
        ProgressCharacter = '#'
    };

    private ProgressBar? ProgressBar { get; set; }

    private const int DownloadBufferSize = 8192;
    private const long DownloadDataPartSize = 800 * FileDownloader.KilobytesInMegabyte;
    private const int MaxRetriesCount = 3;    

    public void DownloadFile(string downloadFileUri, string saveFileName)
    {
        string fileName = Path.GetFileName(saveFileName);

        HttpClientHandler httpClientHandler = new()
        {
            ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => { return true; },
        };

        using HttpClient httpClient = new(httpClientHandler);

        log.Info($"Downloading file {downloadFileUri}");
        log.Info($"Into {saveFileName}");

        string? entryTag = null;
        long? contentLength = null;

        using (var response = httpClient.Send(request: new(HttpMethod.Head, downloadFileUri), completionOption: HttpCompletionOption.ResponseHeadersRead))
        {
            if (response.Headers.AcceptRanges?.Contains("bytes") != true)
                throw new WebException("Сервер не поддерживает получение частичных данных");

            entryTag = response.Headers.ETag?.Tag ?? throw new WebException("Сервер не вернул ETag");
            contentLength = response.Content.Headers.ContentLength ?? throw new WebException($"Null content length given");
        }

        FileStreamOptions fsOptions = new()
        {
            Mode = FileMode.Create,
            Access = FileAccess.Write,
            PreallocationSize = (long)contentLength!,
            BufferSize = FileDownloader.DownloadBufferSize
        };

        this.InitProgress(saveFileName);

        try
        {
            using FileStream fileStream = new FileStream(saveFileName, fsOptions);
            var buffer = new byte[FileDownloader.DownloadBufferSize];

            DateTime downloadStartDateTime = DateTime.Now;

            long totalReadBytes = 0;
            long readLoopCnt = 0;
            int currentRetryCount = 0;

            while (totalReadBytes < contentLength)
            {
                try
                {
                    int currentReadBytes = 0;
                    long currentDataPart = Math.Min(FileDownloader.DownloadDataPartSize, (long)contentLength - totalReadBytes);
                    fileStream.Seek(totalReadBytes, SeekOrigin.Begin);

                    using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, downloadFileUri);
                    request.Headers.IfRange = new RangeConditionHeaderValue(entryTag);
                    request.Headers.Range = new RangeHeaderValue(totalReadBytes, totalReadBytes + currentDataPart);

                    using HttpResponseMessage response = httpClient.Send(request, HttpCompletionOption.ResponseHeadersRead);
                    response.EnsureSuccessStatusCode();
                    using Stream contentStream = response.Content.ReadAsStream();

                    do
                    {
                        currentReadBytes = contentStream.Read(buffer, 0, buffer.Length);

                        if (currentReadBytes > 0)
                        {
                            totalReadBytes += currentReadBytes;
                            readLoopCnt++;

                            fileStream.Write(buffer, 0, currentReadBytes);

                            if (readLoopCnt % 2000 == 0)
                            {
                                /*
                                var currentTime = DateTime.Now - downloadStartDateTime;
                                float currentPerc = totalReadBytes * 100f / (long)contentLength;

                                if (currentPerc - lastReportedPerc > 5)
                                {
                                    TimeSpan estimated = TimeSpan.Zero;

                                    if (currentPerc > 0 && currentTime.TotalSeconds > 10)
                                        estimated = TimeSpan.FromSeconds(currentTime.TotalSeconds * 100 / currentPerc) - currentTime;

                                    Console.WriteLine($"Загружено {totalReadBytes / FileDownloader.KilobytesInMegabyte}/{contentLength / FileDownloader.KilobytesInMegabyte}мб ({currentPerc:f2}%)");

                                    lastReportedPerc = currentPerc;
                                }
                                */

                                this.UpdateProgress(totalReadBytes/ (long)contentLength);
                            }
                        }
                    }
                    while (currentReadBytes > 0);

                    currentRetryCount = 0;
                }
                catch (Exception ex)
                {
                    currentRetryCount++;
                    if (currentRetryCount <= FileDownloader.MaxRetriesCount)
                    {                        
                        log.Warn(ex, $"Ошибка при скачивании файла {fileName}. Попытка {currentRetryCount} из {FileDownloader.MaxRetriesCount}");
                    }
                    else
                    {
                        throw new AggregateException($"Не удалось скачать файл {fileName} после {currentRetryCount} попыток", ex);
                    }
                }
            }

            log.Info($"Файл {fileName} загружен");
        }
        catch (Exception ex)
        {
            log.Fatal(ex, $"Ошибка при загрузке файла {downloadFileUri}");

            try
            {
                if (File.Exists(saveFileName))
                    File.Delete(saveFileName);
            }
            catch (Exception e)
            {
                log.Fatal(e, $"Ошибка удаления недокаченного файла {saveFileName}");
            }

            throw;
        }
    }

    private void InitProgress(string filename)
    {
        this.ProgressBar = new ProgressBar(10000, $"Downloading {Path.GetFileName(filename)}   ", ProcessBarOption);
    }

    private void UpdateProgress(float progressPercentage)
    {
        this.ProgressBar?.Tick((int)(progressPercentage * 100));
    }

    private void KillProgress()
    {
        this.ProgressBar?.Tick(10000);
        this.ProgressBar?.Dispose();
        this.ProgressBar = null;
    }
}